Skip to content

Releases: lispnik/objc

objc 0.4.1

Choose a tag to compare

@lispnik lispnik released this 08 Sep 20:55

Release 0.4.1 -- examples and documentation

src/ is byte for byte what it was at v0.4.0. Anyone depending on #:objc gets
nothing new, which is what the patch number says; this exists so a distribution
has a fixed point to reference rather than a moving master.

OBJC-BLOCK-LIVE-P gains the example it was missing, in audio.lisp -- the only
example whose block outlives the form that made it, because a render block is
held by an engine that will call it until stopped. That is the case
WITH-OBJC-BLOCK cannot serve and the reason MAKE-OBJC-BLOCK is exported. With
it, every exported symbol that should have an example has one; the three that
remain are type descriptors that appear in signatures the runtime hands you and
never in code you write.

doc/demo.md scripts the live-coding demo over canvas.lisp and shader.lisp, with
every form in it run before it was written down -- which changed it three times.
The pitch was "edit in Emacs and watch the window change", and that cannot work:
AppKit needs thread 1, SLY evaluates on a worker, and the entry points refuse.
It is a terminal beside a window, which is the better demo anyway.

934 checks, green on arm64 and Intel.

objc 0.4.0

Choose a tag to compare

@lispnik lispnik released this 08 Sep 20:55

Release 0.4.0 -- six examples, and two tests that were not testing anything

src/ changes here are comments and docstrings only: no behaviour, no
exported symbol. Anyone depending on #:objc gets nothing new. The minor
number is for the size of everything else, not for an API change --
block creation, which is the last real addition, shipped in 0.3.0.

SIX NEW EXAMPLES, chosen by measuring which exported operators no example
used and closing the gap:

collections.lisp -- a Lisp object as a first-class citizen of Cocoa's own
data structures, asked -hash, -isEqual: and -description and answering
from CLOS slots. The CLOS half of the library had no example at all.

browser.lisp -- a class browser over the library's own introspection,
which was exported, documented, and called by nothing but the test
suite.

undo.lisp -- NSUndoManager running Lisp methods as its undo operations,
and the discovery that a forwarding proxy is invisible here:
-prepareWithInvocationTarget: cannot be sent to, because dispatch
resolves the Method first. True of every -forwardInvocation: proxy.

memory.lisp -- retain counts and pools, with deaths observed through
OBJC-OBJECT-DESTROYED rather than inferred. A tagged pointer's retain
count is nonsense and inconsistent nonsense: 2^64-1 from a short
NSString, 2^63-1 from a tagged NSNumber. Draining a pool on the wrong
thread is a memory fault, not a diagnostic. A missing pool is silent
even under OBJC_DEBUG_MISSING_POOLS.

notifications.lisp -- NSNotificationCenter, where the handler runs on
the thread that POSTED rather than the one that registered. A dead
observer is no longer a crash: modern macOS zeroes the reference, which
the folklore has not caught up with.

geometry, strings, task and plugin -- the four that finish the job.
COCOA is now fully exercised and so is every defining macro. A filled
foreign structure passes anywhere its vector does; NSNotFound is
NSIntegerMax and not -1; an NSString counts UTF-16 code units so a Lisp
SUBSEQ at a Cocoa offset is in bounds and wrong; -waitUntilExit before
draining a pipe deadlocks past 64KB; and a typedef is erased at the
boundary.

TWO TESTS WERE PASSING WITHOUT TESTING ANYTHING.
ASDF:SYSTEM-RELATIVE-PATHNAME quotes a wildcard, so "src/*.lisp" named a
file that does not exist and DIRECTORY returned NIL.
ONLY-ABI-LISP-KNOWS-ABOUT-SB-ALIEN -- the test CLAUDE.md describes as
what keeps the seam a seam -- was green with sb-alien: sitting in
src/cocoa.lisp. Both file-scanning tests now enumerate properly and
assert a floor on the count first, and each was verified by breaking
something on purpose. An audit found no test in the suite that makes
zero assertions: 266 tests, 266 ran, 931 checks.

Green on arm64 and Intel.

objc 0.3.1

Choose a tag to compare

@lispnik lispnik released this 08 Sep 20:55

Release 0.3.1 -- four more examples, and no library change at all

src/ is byte for byte what it was at v0.3.0. Anyone depending on #:objc
gets nothing new, which is what the patch number is saying; everything
here is in examples/ and test/.

metal.lisp -- GPU compute. A Metal Shading Language kernel compiled at
run time from a string and run over a Lisp vector, so the GPU program is
data another function wrote. It exercises more of the bridge at once
than anything else: a plain C entry point, message sends, an
owned-object convention, and MTLSize passed by value. Its timing
report corrects the claim people expect -- for element-wise work the GPU
LOSES, because copying dominates, and it wins by 200x or more only once
the arithmetic per element covers two copies.

scene-kit.lisp -- a scene graph built from Lisp forms and rendered to a
PNG with no window, because SCNRenderer draws into an image rather than
a view. Ships no assets; the picture is described where you can read
it.

audio.lisp -- an AVAudioSourceNode whose render block is a Lisp closure,
so the block is the instrument. Tested offline through the engine's
manual rendering mode, which is deterministic and silent and needs no
sound hardware, and demonstrated live through PLAY, which no test calls.
The header records what makes it unlike everything else here: the render
block runs on a real-time thread with a deadline, where a collection
that overruns is audible.

shader.lisp -- one MSL expression per pixel, written to a PNG or
animated in a window through the canvas's draw hook. The canvas's live
loop with the GPU drawing.

Four traps recorded, each found by writing the example rather than by
reasoning about it:

MTLCreateSystemDefaultDevice signals FLOATING-POINT-OVERFLOW. The
bridge masks Cocoa's traps around message sends, and a CFFI:DEFCFUN is
not a message send.

-bitmapData returns `unsigned char *', which encodes exactly as a C
string does, so plain INVOKE hands back "" -- the buffer starts with a
zero byte. INVOKE-INTO with :POINTER is what the manual provides, and
nothing else here had needed it.

-dispatchThreads: rounds the grid up to whole threadgroups, so a
kernel without a bounds check writes past its buffer on any size that
is not a multiple of the group.

An amplitude of 0.5 comes back peaking at 0.5/sqrt(2), because the
main mixer attenuates. Measured; the pan-law explanation is inference
and is labelled as such.

820 checks, green on stock and safepoint builds, both architectures.

objc 0.3.0

Choose a tag to compare

@lispnik lispnik released this 08 Sep 20:55

Release 0.3.0 -- Objective-C blocks

The headline is block creation. This library could pass and receive a
block as an opaque pointer but not make one, which put the
completion-handler half of macOS out of reach. MAKE-OBJC-BLOCK builds a
real Block ABI literal whose invoke function is an alien callable, so
Cocoa calls a Lisp closure; CALL-OBJC-BLOCK goes the other way.
Structures cross by value in both directions. Foundation now sorts an
NSArray through a Lisp comparator, and dispatch_async enters Lisp on a
libdispatch worker thread.

Lifetimes are refcounted through the descriptor's copy and dispose
helpers, so freeing a block Cocoa still holds is safe: the copy took its
own reference. WITH-OBJC-BLOCK is right even for asynchronous work.

This widens the OBJC package past the 42 symbols the LispWorks manual
documents, by 8. LispWorks has no block interface in OBJC at all -- it
lives in the FLI there, and there is no FLI here. The seam test asserts
the exact exported set rather than a count, so the widening had to be
written down rather than absorbed.

Two bugs fixed, both silent, both older than this release:

Every BOOL ARGUMENT was NO. :bool mapped to sb-alien's (boolean 8),
which converts on SBCL's side and wants a generalized boolean, while
every conversion function here produces the integer 1 or 0 -- and
(boolean 8) given 1 arrives as NO. Results were right the whole time,
which is why the suite never noticed. It is (unsigned 8) now.

INVOKE returned a pointer into a freed buffer for a structure result
with no Lisp representation. It read as plausible numbers. It signals
now, and names INVOKE-INTO.

Both were found by writing examples, not by writing tests.

A concurrency limit, measured and then lifted. Two libdispatch worker
threads inside Lisp at once kill a stock SBCL: a GC stops the world by
signalling, and Darwin will not signal a workqueue thread at all -- you
get one for free only because the collector skips the thread that
triggered it. Building SBCL --with-sb-safepoint removes it entirely, and
CI now proves that on every push. PARALLEL-MAP exists and refuses,
rather than crashing, where the build cannot take it.

Ten new examples: GCD, NSURLSession, NaturalLanguage, Core Image, a
dispatch-source file watcher, KVO, NSDataDetector, NSPredicate and
variadic sends, headless PDFKit, Quick Look thumbnails, and NSWorkspace.
Several exist to record a trap rather than to show an API.

798 checks, green on stock and safepoint builds, on both architectures.

objc 0.2.0

Choose a tag to compare

@lispnik lispnik released this 08 Sep 20:55

objc v0.2.0

A live drawing canvas (examples/canvas.lisp): a real NSView subclass whose
-drawRect: calls a Lisp function you redefine at the REPL while the window is
open, drawing with NSColor and NSBezierPath. It puts struct-by-value on the hot
path -- -drawRect: takes an NSRect by value, every shape passes NSRect/NSPoint
by value -- and renders offscreen for a windowless test.

Underneath, large structure returns are routed through objc_msgSend_stret where
the ABI calls for it. CI: dropped the vanished free Intel runner, then took the
Intel label that still exists. And the Status section now says plainly there
will be no FLI compatibility layer.

objc 0.1.0

Choose a tag to compare

@lispnik lispnik released this 02 Sep 18:16

The LispWorks Objective-C interface, reimplemented for SBCL on macOS.

The packages are literally named OBJC and COCOA, the exported symbols have
the LispWorks names and lambda lists, and code written against the LispWorks
Objective-C and Cocoa Interface User Guide and Reference Manual
is intended to
load and run unchanged.

(objc:ensure-objc-initialized)

(objc:invoke (objc:invoke "NSString" "stringWithUTF8String:" "hello world")
             "rangeOfString:" "world")
;; => (6 . 5)

What is here

All 42 symbols of the OBJC package and all 11 of COCOA — including the
parts that are easy to leave out. define-objc-class, define-objc-method and
define-objc-class-method install real IMPs, so Objective-C calls into Lisp;
structures pass and return by value in both directions; define-objc-struct,
define-objc-typedef and define-objc-protocol are there, as is the
standard-objc-object CLOS integration with objc-object-copied and
objc-object-destroyed.

Dispatch is dynamic. A method's type encoding is read from the runtime, parsed,
and turned into a compiled trampoline that calls objc_msgSend through one
exact non-variadic signature; trampolines are memoized by call shape, so the
compiler runs once per distinct shape. That is close to how LispWorks does it.

Every example from the manual is ported and asserted, and
examples/manual.lisp is a near-verbatim copy of the file LispWorks ships —
every objc: form in it unchanged from the original.

On correctness

643 checks, green locally and on a clean CI runner.

Where the manual is ambiguous, stale or self-contradictory, the answer was
settled by running LispWorks Personal 8.1 and recording what it actually did.
Those answers are committed to test/oracle/answers.lisp and asserted against,
so the differential tests run without LispWorks installed. Two examples of what
that caught: invoke returns 1/0 for a BOOL on Apple silicon, not T/NIL,
despite the encoding being C99 _Bool there; and ns-point, ns-size and
ns-range are 64-bit, though the reference pages still describe the 32-bit
layouts.

Known limits

  • An Objective-C exception terminates the process. There is no @try/@catch
    here, and none in LispWorks either. The common case is prevented rather than
    caught: dispatch resolves the Method first, so an unimplemented selector is
    a Lisp error raised before anything is sent.
  • Variadic methods need :variadic-num-of-fixed. On Apple silicon a variadic
    call passes its variable arguments on the stack; without it +stringWithFormat:
    reads garbage. LispWorks fails silently here, this warns once.
  • arm64 only, so far. Nothing is conditionalised on architecture and BOOL's
    encoding is measured from the runtime, so Intel should work — it has simply
    never been run there.
  • SBCL only. Dynamic dispatch is built on sb-alien, confined to
    src/abi.lisp by a test that greps for it, so porting is one file's work.

Getting started

ocicl install
make test
(asdf:load-system :objc/examples)
(objc/examples:run-manual-examples)                                  ; headless
(objc/examples:run-until-closed (objc/examples:test-area-calculator)) ; a window

MIT.