naqvis / webview

Crystal bindings to Webview library
MIT License
93 stars 8 forks source link

Webview run method hangs / isn't recognized by Crystal's fiber scheduler #16

Closed lancecarlson closed 1 year ago

lancecarlson commented 1 year ago

I based this code snippet on something I found on the web and figured this should work, but it doesn't quite.

It starts the kemal server and runs the webview, but when you try to go to http://localhost/ it hangs. This seems to be because wv.run is a c method call and blocks and is not recognized by Crystal's scheduler so the Fiber isn't doing what it needs to do.

Thoughts?

require "kemal"
require "webview"

# ============
# Settings
# ============

# App params
IP     = "127.0.0.1"
PORT   = 3000
WIDTH  =  800
HEIGHT =  600
TITLE  = "My new app"

# Kemal.config.env = "production"

# TODO: Write documentation for `WebviewTest`
module WebviewTest
  VERSION = "0.1.0"

  # TODO: Put your code here
  def self.run
    # ======
    # Server
    # ======

    spawn do
      Kemal.config.port = (ENV["PORT"]? || PORT).to_i
      Kemal.config.host_binding = ENV["HOST_BINDING"]? || "#{IP}"
      # =======
      # Actions
      # =======

      get "/" do
        "hello world"
      end

      Kemal.run
    end

    spawn do
      wv = Webview.window(WIDTH, HEIGHT, Webview::SizeHints::NONE,
        "#{TITLE}",
        "http://#{IP}:#{PORT}/")

      wv.run
      wv.destroy
    end

    Fiber.yield
  end
end

WebviewTest.run
naqvis commented 1 year ago

Crystal by default is single threaded application and invoking external C library (webview) blocks that whole thread. So you will have few options like:

  1. Run your crystal program in (still experimental) multi-threaded mode (mt_preview)
  2. Call wv.run in a separate thread (something like below)
    Thread.new do
    wv.run # probably wv.not_nil!.run
    wv.destroy
    end
  3. Run these two in separate processes (i.e. run Kemal application in one process and webview in another process)
lancecarlson commented 1 year ago

Here is another issue. With a little help from discord, someone came up with an example that might work (though I think it would be nice if you didn't need to spawn a new thread):

require "kemal"
require "webview"

IP     = "127.0.0.1"
PORT   = 3000
WIDTH  =  800
HEIGHT =  600
TITLE  = "My new app"

webview_thread = Thread.new do
  wv = Webview.window(WIDTH, HEIGHT, Webview::SizeHints::NONE,
    "#{TITLE}",
    "http://#{IP}:#{PORT}/")
  wv.run
  wv.destroy
end

get "/" do
  "hello from kemal"
end

Kemal.run

It works on his linux machine, but breaks on my OS X.

[development] Kemal is ready to lead at http://0.0.0.0:3000 Assertion failed: (NSViewIsCurrentlyBuildingLayerTreeForDisplay() != currentlyBuildingLayerTree), function NSViewSetCurrentlyBuildingLayerTreeForDisplay, file NSView.m, line 12845. Program received and didn't handle signal TRAP (5)

lancecarlson commented 1 year ago

Inverting it works:

kemal_thread = Thread.new do
  get "/" do
    "hello from kemal"
  end
  Kemal.run
end

wv = Webview.window(WIDTH, HEIGHT, Webview::SizeHints::NONE,
  "#{TITLE}",
  "http://#{IP}:#{PORT}/")
wv.run
wv.destroy

Any thoughts on the weird NS issues ? ^

naqvis commented 1 year ago

Unfortunately this is a hard dependency as we can't change the code base of webview C library and have it to do things in a separate thread, but this should be something which need to be handled by the Crystal. Other languages like (Golang for example) invoke external library calls in a separate thread, so if Crystal would have done that something similar, developer wouldn't have to deal with such technical details.

lancecarlson commented 1 year ago

@naqvis Think this could be used as an example usage on the README?

naqvis commented 1 year ago

Thanks @lancecarlson. For sure and appreciate your PR