Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
macos:
name: macOS / Ruby ${{ matrix.ruby }}
runs-on: macos-14
strategy:
fail-fast: false
matrix:
ruby: ['3.2', '3.3', '3.4']
steps:
- uses: actions/checkout@v4

- name: Clone libBeresta (read-only) for codegen + native build
run: |
git clone --depth=1 https://github.com/libBeresta/libBeresta.git ../libBeresta

- name: Install build deps (cmake, libpng, zlib, pkg-config)
run: |
brew update
brew install cmake libpng pkg-config

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true

- name: Build libBeresta.dylib
run: |
cmake -S . -B build \
-DLIBBRST_SOURCE_DIR=$(cd ../libBeresta && pwd) \
-DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j

- name: Regenerate FFI bindings from .lsp
run: |
ruby generator/bin/brst-binding-ruby-gen \
--data-dir ../libBeresta/gen/data \
--out-dir lib/brst/binding/ruby

- name: Verify regeneration is idempotent (no diff)
run: |
if ! git diff --quiet -- lib/brst/binding/ruby; then
echo "Regeneration produced a diff — committed bindings drifted from generator output:"
git diff --stat -- lib/brst/binding/ruby
git diff -- lib/brst/binding/ruby
exit 1
fi

- name: Run tests
run: bundle exec rspec

- name: Build gem
run: gem build brst-binding-ruby.gemspec
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
*.gem
/.bundle/
/Gemfile.lock
/coverage/
/pkg/
/tmp/
/spec/tmp/
/spec/smoke/*.pdf
/ext/libBeresta/build/
/ext/libBeresta/install/
/ext/libBeresta/lib/
/build/
*.dylib
*.so
*.bundle
*.rbc
.DS_Store
*.gem
.rspec_status
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--require spec_helper
--format documentation
--color
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Changelog

## [Unreleased]

## [0.1.0] - TBD (pending libBeresta 1.0.0 release)

Initial experimental release.

### Added
- Low-level FFI bindings auto-generated from libBeresta's canonical
S-expression definitions (`gen/data/*.lsp`).
- S-expression parser and Ruby FFI renderer in `generator/`.
- Smoke test that generates a PDF on macOS.
- mandatory baseline: README.md + CMakeLists.txt at repository root
(per libBeresta org-wide `brst-binding-<lang>` family convention).

### Known limitations
- macOS only. Linux support planned for the next release.
- Low-level surface only. An idiomatic high-level API is planned as a
separate gem.

[0.1.0]: https://github.com/libBeresta/brst-binding-ruby/releases/tag/v0.1.0
95 changes: 95 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# brst-binding-ruby — mandatory baseline build entry point
#
# This CMakeLists.txt fulfils the libBeresta org-wide baseline for
# `brst-binding-<lang>` repositories (README + CMakeLists.txt). It exists to
# build the libBeresta shared library so that the Ruby FFI bindings can load
# it at runtime.
#
# Two modes are supported:
#
# 1. Vendored source mode (preferred for `gem install`):
# Drop libBeresta source into ext/libBeresta/ (e.g. via git submodule or
# tarball) and build from this directory:
# cmake -S . -B build
# cmake --build build
#
# 2. External source mode (development convenience):
# Pass -DLIBBRST_SOURCE_DIR=/path/to/libBeresta clone:
# cmake -S . -B build -DLIBBRST_SOURCE_DIR=$HOME/src/libBeresta
# cmake --build build
#
# The resulting shared library (libbrst.dylib / libbrst.so) is copied into
# ext/libBeresta/lib/ where the Ruby loader looks for it.

cmake_minimum_required(VERSION 3.16)
project(brst_binding_ruby NONE)

set(_default_libbrst_src "${CMAKE_CURRENT_LIST_DIR}/ext/libBeresta")
set(LIBBRST_SOURCE_DIR "${_default_libbrst_src}" CACHE PATH
"Path to libBeresta source tree to build (defaults to ext/libBeresta)")

if(NOT EXISTS "${LIBBRST_SOURCE_DIR}/CMakeLists.txt")
message(WARNING
"libBeresta source not found at ${LIBBRST_SOURCE_DIR}.\n"
"Either:\n"
" - Place libBeresta source in ext/libBeresta/ (e.g. git submodule), or\n"
" - Pass -DLIBBRST_SOURCE_DIR=/path/to/libBeresta\n"
"Skipping libBeresta build target.")
return()
endif()

enable_language(C)

set(LIBBRST_SHARED_LIB ON CACHE BOOL "" FORCE)
set(LIBBRST_EXAMPLES OFF CACHE BOOL "" FORCE)
set(LIBBRST_TESTS OFF CACHE BOOL "" FORCE)

add_subdirectory("${LIBBRST_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/libBeresta-build")

set(_outdir "${CMAKE_CURRENT_LIST_DIR}/ext/libBeresta/lib")
file(MAKE_DIRECTORY "${_outdir}")

add_custom_target(brst_binding_ruby_stage ALL
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:brst>
"${_outdir}/libbrst${CMAKE_SHARED_LIBRARY_SUFFIX}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:brst>
"${_outdir}/$<TARGET_FILE_NAME:brst>"
DEPENDS brst
COMMENT "Staging libbrst shared library into ext/libBeresta/lib/")

# ---------------------------------------------------------------------------
# Mandatory libBeresta org-wide CMake targets
#
# Per dmitrys99's guidance (libBeresta/libBeresta#19), every
# brst-binding-<lang> repository exposes three standard targets so that
# cross-language CI/CD pipelines can drive each binding uniformly:
#
# - binding: regenerate the FFI bindings from libBeresta/gen/data/*.lsp
# - check: run the binding's test suite
# - bundle: produce the language-native distribution archive
#
# For Ruby these shell out to ruby / bundle / gem respectively.
# ---------------------------------------------------------------------------

set(_gen_data_dir "${LIBBRST_SOURCE_DIR}/gen/data")
set(_binding_out_dir "${CMAKE_CURRENT_LIST_DIR}/lib/brst/binding/ruby")

add_custom_target(binding
COMMAND ruby
"${CMAKE_CURRENT_LIST_DIR}/generator/bin/brst-binding-ruby-gen"
--data-dir "${_gen_data_dir}"
--out-dir "${_binding_out_dir}"
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
COMMENT "Regenerating FFI bindings from ${_gen_data_dir}")

add_custom_target(check
COMMAND bundle exec rspec
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
COMMENT "Running rspec to verify the bindings")

add_custom_target(bundle
COMMAND gem build brst-binding-ruby.gemspec
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
COMMENT "Building brst-binding-ruby .gem archive")
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source "https://rubygems.org"

gemspec
31 changes: 31 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
brst-binding-ruby — Ruby FFI bindings for libBeresta

Copyright (c) 2026 Kenta Ishizaki and contributors

This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.

Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:

1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.

2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.

3. This notice may not be removed or altered from any source distribution.

----------------------------------------------------------------------

This gem provides bindings to libBeresta (https://github.com/libBeresta/libBeresta),
which is itself distributed under the zlib/libpng license. libBeresta is
originally a fork of libHaru (https://github.com/libharu/libharu), also under
the zlib/libpng license. Original libHaru copyright belongs to Takeshi Kanno
and contributors; libBeresta copyright belongs to Dmitry Solomennikov and
contributors. Bindings here are generated from libBeresta's canonical
S-expression definitions (gen/data/*.lsp).
139 changes: 139 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# brst-binding-ruby

[![CI](https://github.com/libBeresta/brst-binding-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/libBeresta/brst-binding-ruby/actions/workflows/ci.yml)

**Experimental, low-level Ruby FFI bindings for [libBeresta][libBeresta]**, a
free, cross-platform PDF generation C library forked from [libHaru][libHaru].

This is the Ruby member of the libBeresta org's `brst-binding-<lang>` family of
language bindings. Bindings are auto-generated from libBeresta's canonical
S-expression definitions in [`gen/data/*.lsp`][gen-data], so they stay faithful
to the upstream API.

## Status: v0.1.0 — experimental

- Auto-generated low-level FFI surface from `gen/data/*.lsp`.
- macOS-only at this version. Linux support is planned for the next release.
- An idiomatic, higher-level Ruby API is **not** part of this gem; that is
planned as a separate gem on top of these low-level bindings.
- Released in lockstep with libBeresta 1.0.0.

## Mandatory baseline

Per the libBeresta org-wide convention for `brst-binding-<lang>` repositories,
this repository ships:

- `README.md` (this file)
- `CMakeLists.txt` — entry point that builds libBeresta (vendored or external)
into a shared library that the Ruby loader can `dlopen`.

Everything else (gemspec, `lib/`, `spec/`, generator, CI) follows Ruby
community conventions on top of that baseline.

## Quick start

### Install (development checkout)

This v0.1.0 is not yet on rubygems.org. To try it from a checkout:

```sh
git clone https://github.com/libBeresta/brst-binding-ruby.git
cd brst-binding-ruby
git clone --depth=1 https://github.com/libBeresta/libBeresta.git ../libBeresta
brew install cmake libpng # macOS native deps
bundle install
cmake -S . -B build -DLIBBRST_SOURCE_DIR=$(cd ../libBeresta && pwd) \
-DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j
bundle exec rspec
```

### Hello PDF in Ruby

```ruby
require "brst/binding/ruby"

include Brst::Binding::Ruby

pdf = Base.BRST_Doc_New(nil, nil)
page = DocPage.BRST_Doc_Page_Add(pdf)
PageRoutines.BRST_Page_SetSize(page, :A4, :PAGE_ORIENTATION_PORTRAIT)

font = DocFont.BRST_Doc_Font(pdf, "Helvetica", nil)
Text.BRST_Page_BeginText(page)
Text.BRST_Page_SetFontAndSize(page, font, 20.0)
Text.BRST_Page_MoveTextPos(page, 50.0, 750.0)
Text.BRST_Page_ShowText(page, "Hello, Beresta from Ruby!")
Text.BRST_Page_EndText(page)

DocSave.BRST_Doc_SaveToFile(pdf, "hello.pdf")
Base.BRST_Doc_Free(pdf)
```

## How it's built

```
libBeresta/gen/data/*.lsp (S-expression — Source of Truth)
generator/bin/brst-binding-ruby-gen (Ruby S-exp parser + FFI renderer)
lib/brst/binding/ruby/types.rb (consolidated typedefs / enums / structs)
lib/brst/binding/ruby/<file>.rb (one FFI module per .lsp file)
ext/libBeresta/lib/libbrst.dylib (cmake-built native library)
```

The generator parses S-expressions directly. The `gen/json/*.json` files are
themselves auto-generated from the same `.lsp` files and are useful for
structural cross-checking but are **not** the source of truth.

To regenerate after pulling new `.lsp` data:

```sh
bundle exec rake generate
# or:
ruby generator/bin/brst-binding-ruby-gen \
--data-dir ../libBeresta/gen/data \
--out-dir lib/brst/binding/ruby
```

CI guards against generator drift: the workflow regenerates on every push and
fails if the committed bindings differ from the generator's output.

## Library resolution

At load time, `Brst::Binding::Ruby::Library` searches in this order:

1. `ENV["BRST_BINDING_RUBY_LIB"]` — explicit override (path or library name).
2. `ext/libBeresta/lib/libbrst.{dylib,so}` — vendored build output.
3. `ext/libBeresta/build/src/libbrst.{dylib,so}` — in-tree cmake build output.
4. `build/libBeresta-build/src/libbrst.{dylib,so}` — alternative in-tree.
5. Falls back to `libbrst` / `brst` via the system loader.

If a function or type referenced by upstream `.lsp` data is absent from the
loaded native library (e.g. caption / C-symbol drift, or a feature compiled
out of this build), the corresponding `attach_function` is recorded in
`<Module>::MISSING_SYMBOLS` and the rest of the gem still loads.

## Acknowledgements

- [@dmitrys99][dmitrys99] — libBeresta maintainer, designer of the S-expression
source-of-truth pipeline, and gracious mentor for this binding effort. The
`brst-binding-<lang>` org structure and the `README + CMakeLists.txt`
baseline come from his guidance.
- [libHaru][libHaru] — original PDF C library this work ultimately descends
from. Original copyright belongs to Takeshi Kanno and contributors.
- Russian-language docs in the upstream `.lsp` data are preserved verbatim in
the parsed tree but not surfaced in v0.1.0 bindings.

## License

[zlib/libpng License](LICENSE) — same as libBeresta and libHaru.

[libBeresta]: https://github.com/libBeresta/libBeresta
[libHaru]: https://github.com/libharu/libharu
[gen-data]: https://github.com/libBeresta/libBeresta/tree/master/gen/data
[dmitrys99]: https://github.com/dmitrys99
19 changes: 19 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
require "bundler/gem_tasks"

begin
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
rescue LoadError
# rspec is a dev dependency; fine if absent in production
end

desc "Regenerate FFI bindings from libBeresta gen/data/*.lsp"
task :generate, [:gen_data_dir] do |_, args|
data_dir = args[:gen_data_dir] || ENV["BRST_GEN_DATA_DIR"] ||
File.expand_path("../libBeresta/gen/data", __dir__)
ruby "-Igenerator/lib", "generator/bin/brst-binding-ruby-gen",
"--data-dir", data_dir,
"--out-dir", "lib/brst/binding/ruby"
end

task default: :spec
Loading
Loading