JMM’s notes on

Guile

I knew I’d get back into Scheme some day…

Hello world

The nixpkg to use is guile.

Here’s a script you can run (adapted for Nix using env):

#!/usr/bin/env -S guile -s
!#
(display "Hello, world!")
(newline)

Or you can use sh with exec:

#!/usr/bin/env sh
exec guile -e main -s "$0" "$@"
!#
(define (main args)
  (display "Hello world")
  (newline))

Nix

Here’s how to use Nix with direnv (see also /notes/direnv) with several packages. (You can also just run nix-shell -p guile_3_0 guile-json guile-gnutls and that’ll work too.)

  1. Make a file shell.nix with content like:
    { pkgs ? import <nixpkgs> {} }:
    
    pkgs.mkShell {
      buildInputs = with pkgs; [ guile_3_0 guile-json guile-gnutls ];
    }
    
  2. Make a file .envrc with content like:
    #!/usr/bin/env bash
    
    use nix
    
    # Local Variables:
    # mode: sh
    # sh-shell: bash
    # End:
    
  3. (Optional)
    If you want to verify that guile can see Nix’s dependencies, look at %load-path within guile or look at the environment variables GUILE_LOAD_PATH, GUILE_EXTENSIONS_PATH, GUILE_LOAD_COMPILED_PATH.

Emacs

Install Geiser and use M-x geiser-mode (a minor mode) on top of M-x scheme-mode. Launch a REPL with M-x geiser.

Interfacing with C

Haptic/Vibration C library example

Sometimes you want to write some helper functions in C but use them in Guile. For example, you want to trigger the haptic/vibration motor on the Librem 5, which is easier to express in C. Here’s an example of doing that, which was generated with the help on an LLM. I’ve tested it, it works. Pretty fun.

Make your C library, here’s some code saved as haptic.c which you compile with gcc -shared -fPIC -o libhaptic.so haptic.c. It’s been a while since I’ve really used C, so I’m not sure how idiomatic it is.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
#include <sys/ioctl.h>

/* Compile with:
   gcc -shared -fPIC -o libhaptic.so haptic.c

   TODO: Maybe pass in fd directly instead of opening it each time?
 */
int haptic_vibrate(const char *device_path, unsigned short strong_mag,
                   unsigned short weak_mag, unsigned short duration_ms)
{
    struct ff_effect effect;
    struct input_event play;
    int fd, ret;

    fd = open(device_path, O_RDWR);
    if (fd < 0) return -1;

    memset(&effect, 0, sizeof(effect));
    effect.type                    = FF_RUMBLE;
    effect.id                      = -1;
    effect.u.rumble.strong_magnitude = strong_mag;
    effect.u.rumble.weak_magnitude   = weak_mag;
    effect.replay.length           = duration_ms;
    effect.replay.delay            = 0;

    if (ioctl(fd, EVIOCSFF, &effect) < 0) { close(fd); return -2; }

    memset(&play, 0, sizeof(play));
    play.type  = EV_FF;
    play.code  = (unsigned short)effect.id;
    play.value = 1;
    write(fd, &play, sizeof(play));

    /* Wait slightly longer than the effect duration. */
    usleep((unsigned int)duration_ms * 1000 + 100);

    play.value = 0;
    write(fd, &play, sizeof(play));
    ioctl(fd, EVIOCRMFF, effect.id);
    close(fd);
    return 0;
}

Now here’s some example Guile code that uses it:

#!/usr/bin/env guile
!#

(use-modules (system foreign)
             (system foreign-library))

(define libhaptic (load-foreign-library "./libhaptic.so"))

(define haptic-vibrate
  (foreign-library-function
    libhaptic "haptic_vibrate"
    #:return-type int
    #:arg-types (list '* uint16 uint16 uint16)))

(define default-device "/dev/input/by-path/platform-vibrator-event")

(let* ((device "/dev/input/by-path/platform-vibrator-event")
       (ret (haptic-vibrate (string->pointer device)
                            #xFFFF   ; strong magnitude
                            #x0000   ; weak magnitude
                            300)))   ; duration ms
  (if (< ret 0)
      (format #t "Error: ~a~%" ret)
      (format #t "Vibrated OK~%")))

(define (haptic1 durationms)
  (haptic-vibrate (string->pointer default-device)
		  #xFFFF
		  #x0000
		  durationms))

(define (sleepms durationms)
  (usleep (* durationms 1000)))

;; (for-each (lambda (_) (haptic-vibrate (string->pointer default-device) #x9000 #x0000 200) (sleepms 14800)) (iota 10))

(define (minute-buzzer num)
  (begin
    (for-each (lambda (_) (haptic-vibrate (string->pointer default-device) #x9000 #x0000 200) (sleepms 59800)) (iota num))
    (haptic-vibrate (string->pointer default-device) #xFFFF #x0000 1000)))

It might be better to open the file from Guile and then pass a file descriptor, but at least as a first pass this works. It’s pretty fun because you can start to do more fun logic with Scheme than C, and you can play with it interactively from a REPL.

Misc